/*
 * This file is part of the OWASP Proxy, a free intercepting proxy library.
 * Copyright (C) 2008-2010 Rogan Dawes <rogan@dawes.za.net>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to:
 * The Free Software Foundation, Inc., 
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

package org.owasp.proxy.io;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.owasp.proxy.util.CircularByteBuffer;

/**
 * This class processes streams encoded according to RFC2616 "chunked"
 * specification.
 * 
 * This class will either return decoded chunks, or will return the original
 * stream, but return EOF when the last chunk and trailer have been read (if raw
 * == true).
 * 
 * @author Rogan Dawes
 */
public class ChunkedInputStream extends FilterInputStream {

	private boolean raw = false;

	private boolean eof = false;

	private byte[] b = new byte[1024];

	private CircularByteBuffer buffer = new CircularByteBuffer(1024);

	public ChunkedInputStream(InputStream in) throws IOException {
		super(in);
		readChunk();
	}

	/**
	 * Running with raw = true returns the individual chunks, but still shows
	 * eof when the last chunk has been read, rather than continuing into the
	 * next message
	 * 
	 * @param in
	 * @param raw
	 * @throws IOException
	 */
	public ChunkedInputStream(InputStream in, boolean raw) throws IOException {
		super(in);
		this.raw = raw;
		readChunk();
	}

	private int addRaw(int i) {
		if (raw && i != -1) {
			buffer.add((byte) i);
		}
		return i;
	}

	private void readChunk() throws IOException {
		String line = readLine();
		try {
			int semi = line.indexOf(';');
			if (semi > -1)
				line = line.substring(0, semi);
			int size = Integer.parseInt(line.trim(), 16);
			int got, read = 0;
			while (read < size
					&& (got = in.read(b, 0, Math.min(b.length, size - read))) > 0) {
				buffer.add(b, 0, got);
				read = read + got;
			}
			if (read < size)
				throw new IOException(
						"Unexpected end of stream reading a chunk of " + size);
			if (size > 0) {
				// read the trailing line feed after the chunk body,
				// but before the next chunk size
				if (!"".equals(line = readLine()))
					throw new IOException(
							"Unexpected characters reading the trailing CRLF : "
									+ line);
			} else {
				discardTrailer();
				eof = true;
			}
		} catch (NumberFormatException nfe) {
			IOException ioe = new IOException("Error parsing chunk size from '"
					+ line);
			ioe.initCause(nfe);
			throw ioe;
		}
	}

	private boolean isEmptyBuffer() {
		return buffer == null || buffer.length() == 0;
	}

	public int read() throws IOException {
		if (eof && isEmptyBuffer())
			return -1;
		int i = buffer.remove();
		while (!eof && isEmptyBuffer())
			readChunk();
		return i;
	}

	public int read(byte[] b, int off, int len) throws IOException {
		if (eof && isEmptyBuffer())
			return -1;
		int got = buffer.remove(b, off, len);
		while (!eof && isEmptyBuffer())
			readChunk();
		return got;
	}

	public int available() throws IOException {
		return buffer == null ? 0 : buffer.length();
	}

	public boolean markSupported() {
		return false;
	}

	private String readLine() throws IOException {
		StringBuilder line = new StringBuilder();
		int i = addRaw(in.read());
		while (i > -1 && i != '\r' && i != '\n') {
			line = line.append((char) (i & 0xFF));
			i = addRaw(in.read());
		}
		if (i == '\n') {
			throw new IOException("Unexpected LF, was expecting a CR first");
		} else if (i == '\r') {
			i = addRaw(in.read());
			if (i != '\n')
				throw new IOException("Unexpected character "
						+ Integer.toHexString(i) + ", was expecting 0x0A");
		}
		return line.toString();
	}

	/**
	 * This actually discards the trailer, since it is available for use via the
	 * raw content, if desired
	 * 
	 * @throws IOException
	 */
	private void discardTrailer() throws IOException {
		while (!"".equals(readLine()))
			;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.io.FilterInputStream#close()
	 */
	@Override
	public void close() throws IOException {
		// do nothing
	}

}
